﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Net
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics.CodeAnalysis;
    using System.IO;
    using System.Text;

    using Phuse.IO;
    using Phuse.Text;

    /// <summary>Represents a stream, through which the response to a single stage command can be read.</summary>
    /// <remarks>The first line of the response is accessible through the <see cref="InitialLineWords"/> property.
    /// Possibly present subsequent lines of the response can be retrieved through the <see cref="Read"/> method.
    /// </remarks>
    /// <threadsafety static="true" instance="false"/>
    public sealed class ResponseStream : BufferStream
    {
        private readonly ReadOnlyCollection<string> initialLineWords;
        private State state = State.Done;

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>Gets a list containing the words of the initial line of the response.</summary>
        /// <remarks>
        /// <para>The list contains exactly as many elements as were specified at construction, see
        /// <see cref="CreateResponseStreamCallback"/>.</para>
        /// <para>If the actual word count of the initial response line is smaller than the list count then an
        /// appropriate number of elements at the end are left <c>null</c>. If the word count is greater, then the last
        /// element of the list contains the last expected word plus all possibly following unexpected words.</para>
        /// <para>All initial lines of responses from the server must start with a response code, which is available at
        /// index 0 of the returned list.</para>
        /// </remarks>
        public IList<string> InitialLineWords
        {
            get
            {
                return this.initialLineWords;
            }
        }

        /// <summary>See <see cref="Stream.Read"/>.</summary>
        /// <remarks>For multi-line responses, the bytes retrieved through this method have the dot-stuffing already
        /// undone, see <a href="http://www.ietf.org/rfc/rfc3977.txt">RFC 3977</a> 3.1.1.</remarks>
        [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification = "Validated with call to BufferHelper.AssertValidRange.")]
        public sealed override int Read(byte[] buffer, int offset, int count)
        {
            this.AssertNotDisposed();
            BufferHelper.AssertValidRange(buffer, "buffer", offset, "offset", count, "count");

            // Letting the code run as unchecked leads to better performance in the device emulator. On the desktop
            // performance does not seem to be affected.
            unchecked
            {
                ReadBuffer readBuffer = this.ReadBuffer;
                int index = offset;
                int pastEnd = offset + count;
                byte currentByte;

                while ((this.state != State.Done) && (index < pastEnd) &&
                    ((readBuffer.Index < readBuffer.Count) || readBuffer.Fill()))
                {
                    currentByte = readBuffer[readBuffer.Index++];

                    switch (this.state)
                    {
                        case State.LastWasCarriageReturn:
                            this.state = State.LastWasLineFeed;
                            break;
                        case State.LastWasLineFeed:
                            switch (currentByte)
                            {
                                case (byte)'\r':
                                    this.state = State.LastWasCarriageReturn;
                                    break;
                                case (byte)'.':
                                    this.state = State.LastWasDotAtLineStart;
                                    continue;
                                default:
                                    this.state = State.InDataLine;
                                    break;
                            }

                            break;
                        case State.InDataLine:
                            if (currentByte == (byte)'\r')
                            {
                                this.state = State.LastWasCarriageReturn;
                            }

                            break;
                        case State.LastWasDotAtLineStart:
                            if (currentByte == (byte)'\r')
                            {
                                this.state = State.LastWasFinalCarriageReturn;
                                continue;
                            }
                            else
                            {
                                this.state = State.InDataLine;
                            }

                            break;
                        case State.LastWasFinalCarriageReturn:
                            this.state = State.Done;
                            continue;
                    }

                    buffer[index++] = currentByte;
                }

                return index - offset;
            }
        }

        /// <summary>See <see cref="Stream.Write"/>.</summary>
        /// <exception cref="NotSupportedException">Thrown with each call.</exception>
        public sealed override void Write(byte[] buffer, int offset, int count)
        {
            throw CreateNotSupportedException();
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        internal ResponseStream(
            ReadBuffer readBuffer,
            Func<IList<string>, bool> isMultilineCallback,
            Encoding initialLineEncoding,
            int maxInitialLineWordCount) :
            base(readBuffer, null)
        {
            if (initialLineEncoding == null)
            {
                throw new ArgumentNullException("initialLineEncoding");
            }

            if (maxInitialLineWordCount < 2)
            {
                throw new ArgumentOutOfRangeException(
                    "maxInitialLineWordCount", "A number greater than 1 is required.");
            }

            string[] words = new string[maxInitialLineWordCount];
            this.initialLineWords = new ReadOnlyCollection<string>(words);
            readBuffer.Index =
                EncodedBufferHelper.GetFirstLineWords(readBuffer.GetLine(), initialLineEncoding, words, true);

            if (isMultilineCallback(this.initialLineWords))
            {
                this.state = State.LastWasLineFeed;
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        /// <summary>See <see cref="Stream.Dispose(bool)"/>.</summary>
        /// <remarks>Reads and discards any bytes that are possibly left.</remarks>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Dispose() must never throw.")]
        protected sealed override void Dispose(bool disposing)
        {
            try
            {
                if (disposing && !this.IsDisposed && (this.state != State.Done))
                {
                    byte[] buffer = new byte[Settings.InMemoryStreamBufferSize];

                    while (this.Read(buffer, 0, buffer.Length) > 0)
                    {
                    }
                }
            }
            catch
            {
            }
            finally
            {
                base.Dispose(disposing);
            }
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private enum State
        {
            LastWasCarriageReturn,
            LastWasLineFeed,
            InDataLine,
            LastWasDotAtLineStart,
            LastWasFinalCarriageReturn,
            Done
        }
    }
}
